Input-Output 读写的原理与模型
IO 读写的基础原理
Read 和 Write
用户程序在进行 IO 读写时依赖于操作系统底层的 IO 读写,也就是 read 和 write 两大系统的调用。
对于调用操作系统的 read,是把数据从内核缓冲区复制到用户缓冲区的操作;而 write 则是把数据从用户缓冲区复制到内核缓冲区的操作。
Read 和 write 两大系统的调用都不会负责数据在内核缓冲区和物理设备对的交换,这种底层的读写交换都是由操作系统内核(Kernel)完成的。
内存缓冲区
缓冲区的目的是为了减少频繁地和设备之间的物理交换,以及用于在进程与内核之间传递数据和信息的存储单元,以实现高效的数据传输处理。在发生系统中断( 让硬件在需要的时候向内核发出信号(变内核主动为硬件主动))时,需要保存中断之前的进程数据和状态等信息,而在结束中断之后还需要回复之前保存的进程数据和状态信息。为了减少这种底层系统的时间损耗和性能损耗,于是内存缓冲区就诞生了。底层的操作会对内核缓冲区进行监控,当缓冲区达到一定数量时再进行 IO 设备的中断处理。至于什么时候中断(读、写中断)由操作系统的内核来决定,用户程序可以通过轮询或者阻塞等方式获知 IO 操作的结果,按时用户程序没有决定中断的权利,中断的触发和决定都是由内核完成的
IO 的读写操作大多并没有进行实际的 IO 操作,而是在进程缓冲区和内核缓冲区之间进行数据交换
Netty、Redis、Zookeeper高并发实战#2.1 IO读写的基础原理
Socket 的通信
是由操作系统通过网卡获取到所有 socket 分组后被复制到内核缓冲区。这个由操作系统自动完成。
Netty、Redis、Zookeeper高并发实战#^506504175-7GVyeTgpd
四种主要的 IO 模型
四种主要的 IO 模型分别为 同步阻塞 IO、同步非阻塞 IO、IO 多路复用、异步 IO
阻塞和非阻塞
阻塞指的是调用线程一直在等待,而不能干别的事情。
非阻塞是指用户空间的程序不需要等待内核 IO 操作彻底完成,而是拿到内核返回的状态值就返回自己的空间,IO 操作能做就做,不能做就干其他事
Netty、Redis、Zookeeper高并发实战#^26174369-18-1201-1287
阻塞 IO 和非阻塞 IO
阻塞 IO 指的是线程在调用 IO 操作时需要等待内核缓冲区的数据准备好后才能返回用户空间执行用户操作,在这个期间内线程都是阻塞状态.
非阻塞 IO,指的是用户空间的程序不需要等待内核 IO 操作彻底完成,可以立即返回用户空间执行用户的操作,即处于非阻塞的状态,与此同时内核会立即返回给用户一个状态值
Netty、Redis、Zookeeper高并发实战#^26174369-18-701-796
Netty、Redis、Zookeeper高并发实战#^26174369-18-1087-1166
同步 IO 和异步 IO
同步 IO,是一种用户空间和内核空间 IO 发起方式,指的是用户空间的线程是主动发起 IO 调用的一方,内核是被动接受方。
异步 IO 则是反过来,内核是主动调用的一方,用户空间的线程是被调用的一方。
Netty、Redis、Zookeeper高并发实战#^26174369-18-867-971
同步阻塞 IO
我们已经介绍过了同步 IO 和阻塞 IO,我们可以联想一下同步阻塞 IO 是不是就是这俩组合起来呢?也就是由用户空间的线程发起主动 IO 调用,在内核缓存区数据没有准备好时这个线程将阻塞挂起,直到内核缓存区的操作彻底完成后这个线程才回到用户空间。
同步阻塞 IO 的优点
同步阻塞 IO 十分的简单易用,程序员不需要太多的处理,只需要调用对应 API 即可进行 IO 操作
同步阻塞 IO 的缺点
每一次 IO 调用都需要创建一个调用线程,而线程在等待内核执行操作时是阻塞的,虽然阻塞时线程是挂起的并不需要消耗很多 CPU 资源,但是在高并发场景下这样意味着短时间内会创建大量线程来维护网络连接,内存、线程切换的开销会非常巨大。
同步非阻塞 IO
同步非阻塞 IO 下应用的线程需要不断地进行 IO 系统的调用,轮询数据是否已经准备好,如果没有准备好久继续轮询,直到完成 IO 系统效用位置。在线程发起 IO 系统调用时,如果内核还在等待数据则线程立即返回,如果内核数据已经准备则线程会阻塞住等待内核缓冲区的操作彻底完成后返回用户空间。
同步非阻塞 IO 的优点
线程在内核层面做到了异步,内核在等待数据时线程会立即返回进行其他操作,减少了 IO 操作的阻塞时间
同步非阻塞 IO 的缺点
线程会不断轮询内核,占用大量的 CPU 时间,效率低下。在高并发场景中性能非常的低。
IO 多路复用
IO 多路复用是使用 select 或 epoll、poll 同时箭筒多个 IO 事件,通过将多个 IO 请求交给内核进行监听。和 同步非阻塞 Input-Output 类似多路复用 IO 也需要轮询。负责 selec 状态查询的线程需要不断的对 select 进行轮询,当查出 select 下有 IO 操作就绪时进行 IO 操作,将数据从内核缓冲区复制到用户缓冲区
IO 多路复用流程
- 选择器注册。先将需要 read 操作的目标文件描述符(socket 连接)提前注册在选择器中,在 about_Java 中式 Selector 类。然后开启 IO 多路复用的轮询流程
- 就绪状态的轮询。通过选择器查询所有注册过的文件描述符的 IO 就绪状态。通过查询的系统调用,内核会返回一个就绪的 socket 列表。任何一个 socket 准备好了就代表内核缓存区有数据了。
- 用户线程获得就绪的 socket 列表后,根据其中的 socket 连接发起 read 调用,用户线程阻塞。内核将数据从内核缓冲区中复制到用户缓冲区。
- 复制完成后,内核返回结果,用户线程才会解除阻塞的状态,用户线程读取到了数据,继续执行
IO 多路复用的优点
- 可以同时处理多个 IO 事件,从而提高程序的并发性
- 一个线程可以处理多个 IO 事件,减少了系统调用和线程切换和维护的开销,提高了系统性能
IO 多路复用的缺点
本质上 select、epoll 系统效用还是阻塞的,属于同步 IO。需要在读写事件就绪后由系统调用本身负责读写,也就是说读写过程依然是阻塞的。要彻底的解除线程的阻塞,就必须使用 异步 Input-Output
异步 IO
用户线程通过系统调用,向内核注册某个 IO 操作。内核在整个 IO 操作(包括数据准备、数据复制)完成后,通知用户程序,用户执行后续的业务操作。
Netty、Redis、Zookeeper高并发实战#^26174369-18-7286-7364
异步 IO 在内核等待数据和复制数据的两个阶段,用户线程都不是阻塞的。用户线程需要接收内核的 IO 操作完成事件,或者用户线程注册一个 IO 操作的回调函数,由内核完成操作后进行调用。而用户线程在执行 aio_read 调用后会立即返回,不会被阻塞。